/*
*
*	Fetch (a stand-alone) CSS Querying Engine, by Stephen Stchur
*
*	Copyright (c) Microsoft Corporation.  All rights reserved.
*
*	Usage:
*	fetch(cssSelector);			// e.g.  fetch('div > p') returns an array of <p> elements, all of whom have <div> ancestors
*
*/

var fetch = function()
{
	var callback = 'xyz';

	function CacheBit(name)
	{
		this.name = name;
		this.f = 0;
		this.t = 1;
		this.inc = function()
		{
			this.f = (this.t += 2) - 1;
		}
	}
	
	var uniqueBit = new CacheBit('__$gimme$_unique__');
	var genSibBit = new CacheBit('__$gimme$_genSib__');
	var nthBit = new CacheBit('__$gimme$_nth__');

	function search(collection, relationship, ss)
	{
		return rules[relationship](collection, ss);		
	}
	
	function getDescById(root, id)
	{
		var elem = document.getElementById(id);
		if (root === document) return elem;
		var desc = elem;
		while ((desc = desc.parentNode) && desc !== root);
		return desc === root && elem || null;
	}

	function simpleFetch(ss, root)
	{
		root = root || document;
		
		if (ss.id)
		{
			return filter([ getDescById(root, ss.id) ], ss);
		}
		
		delete ss.id;
		var elems = root.getElementsByTagName(ss.tag || '*');
		
		// optimization technique, TODO: make this generic
		if ((!ss.classes || ss.classes.length < 1)
			&& (!ss.attributes || ss.attributes.length < 1)
			&& (!ss.pseudos || ss.pseudos.length < 1))
			//&& (!rules.customExists))
		{
			return makeArray(elems);
		}
		
		var matched = [];
		var i = elems.length;
		while (i--)
		{
			processSelector(ss, elems[i]) && matched.push(elems[i]) && callback && callback(elems[i]);
		}

		return matched;	
	}

	function processSelector(ss, elem)
	{
		if (!elem) return false;
		
		var props = ss.propNames, i = props.length;
		while (i--)
		{
			if (!rules[props[i]](elem, ss[props[i]])) return false;
		}
		return true;
		
		/*
		var rule;
		for (rule in ss)
		{
			if (ss.hasOwnProperty(rule) && !rules[rule](elem, ss[rule])) return false;
		}
		return true;
		*/
		
		/*
		return elem
			&& (!ss.id || rules['id'](elem, ss.id))
			&& (!ss.tag || rules['tag'](elem, ss.tag))
			&& (!ss.classes || ss.classes.length === 0 || rules['classes'](elem, ss.classes))
			&& (!ss.attributes || ss.attributes.length === 0 || rules['attributes'](elem, ss.attributes))
			&& (!ss.pseudos || ss.pseudos.length === 0 || rules['pseudos'](elem, ss.pseudos));
		*/
	}
	
	var rules = 
	{
		'id': function(elem, id)
		{
			return elem.id === id;
		},
		
		'tag': function(elem, tag)
		{
			return elem.tagName === tag || tag === '*';
		},
		
		'classes': function(elem, classes)
		{
			var className = elem.className;
			if (typeof className !== 'string')
			{
				className = readAttribute(elem, 'class');
			}
			
			if (!className)
			{
				return false;
			}

			var elemClass = ' ' + className + ' ';
			var i = classes.length;
			while (i--)
			{
				if (elemClass.indexOf(' ' + classes[i] + ' ') === -1)
				{
					return false;
				}
			}
			return true;
		},
				
		'attributes': function()
		{
			var pah = {};			// parsed attribute hash
			var attrRules =
			{
				'parse': function(_attr)
				{
					var m = _attr.match(/^\[([\w-_]+)?(\=|\^=|\$=|\*=|\|=|~=|!=)?(.*)\]$/);
					return pah[_attr] = { name: m && m[1], delim: m && m[2], val: m && m[3].replace(/^"|"$/g, '') };
				},
				
				'=': function(_attr, _val)
				{
					return _attr === _val;
				},

				'^=': function(_attr, _val)
				{
					return _attr.indexOf(_val) === 0;
				},

				'$=': function(_attr, _val)
				{
					var lio = _attr.lastIndexOf(_val);
					return lio !== -1 && lio + _val.length === _attr.length;
				},

				'*=': function(_attr, _val)
				{
					return _attr.indexOf(_val) !== -1;
				},

				'|=': function(_attr, _val)
				{
					return (_attr === _val) || (_attr.indexOf(_val + '-') === 0);
				},

				'~=': function(_attr, _val)
				{
					return (_attr === _val) || contains(_attr.split(' '), _val);
				},

				'!=': function(_attr, _val)
				{
					return _attr !== _val;
				},

				'unknown': function()
				{
					return false;
				}			
			};
		
			return function(elem, attributes)
			{
				var i = attributes.length;
				while (i--)
				{
					var attr = pah[attributes[i]] || attrRules.parse(attributes[i]);
					var actualVal = readAttribute(elem, attr.name);
					
					if (!actualVal) return false;
					if (!attr.delim) continue;					
					if (!attrRules[attr.delim || 'unknown'](actualVal, attr.val)) return false;
				}
				return true;
			};
		}(),
		
		'pseudos': function()
		{
			var pph = {};			// parsed pseudo hash
			var pseudoRules = 
			{
				parse: function(_pseudo)
				{
					// [0] = entire string, including parenthetical param (if present)
					// [1] = full pseudo name, not including parenthetical params (even if present)
					// [2] = entire param string (including parens)
					// [3] = entire param string (excluding parens)
					// [4] = an in an + b (if present)
					// [5] = a in an + b (if present)
					// [6] = b in an + b (if present)
					var m = _pseudo.match(/([\w-_]+)\b(\((((-?\d*)n)?([+-]?\d+)?|.*)\))?/);
					var pseudo =
					{
						name: m && m[1],
						param:
						{
							wholeValue: m && m[3],
							a: m && m[5] || 0,
							b: m && m[6] || 0
						}
					}					
					
					if (pseudo.param.wholeValue === 'even') { pseudo.param.a = 2; pseudo.param.b = 0; }
					else if (pseudo.param.wholeValue === 'odd') { pseudo.param.a = 2; pseudo.param.b = 1; }
					
					return pph[_pseudo] = pseudo;
				},
				
				'first-child': child('previous'),
				
				'last-child': child('next'),
				
				'only-child': function(_elem)
				{
					return pseudoRules['first-child'](_elem) && pseudoRules['last-child'](_elem);
				},
				
				'nth-child': function(_elem, _pseudo)
				{
					var cacheConst = 10000;
					var currentTrue = nthBit.t * cacheConst;
					var bitName = nthBit.name;
					
					var parent = _elem.parentNode;
					
					var pos = 0;
					if (parent)
					{
						if (parent[bitName + ':parent'] !== nthBit.t)
						{
							parent[bitName + ':parent'] = nthBit.t;
							
							var pos = 0;
							var siblings = parent.children || parent.childNodes;
							var i = -1, len = siblings.length;
							while (++i < len)
							{
								if (siblings[i].nodeType === 1)
								{
									pos++;
									siblings[i][bitName] = currentTrue + pos;
								}
							}
						}
						
						var x = _elem[bitName] - currentTrue - _pseudo.param.b;
						return !_pseudo.param.a && _pseudo.param.b ? x === 0 : (_pseudo.param.a * x >= 0) && (x % (_pseudo.param.a || 1) === 0); 			
					}
					return false;					
				},

				'contains': function(_elem, _pseudo)
				{
					return (_elem.textContent || _elem.innerText || _elem.innerHTML).indexOf(_pseudo.param.wholeValue) !== -1;
				},

				'not': function()
				{
					return false;
				}
			};
			
			function child(direction)
			{
				var sibling = direction + 'Sibling';

				return function(el)
				{
					var sib = el[sibling];
					while (sib && sib.nodeType !== 1)
					{
						sib = sib[sibling];
					}
					return !sib;
				}
			}
				
			return function(elem, pseudos)
			{
				var i = pseudos.length;
				while (i--)
				{
					var pseudo = pph[pseudos[i]] || pseudoRules.parse(pseudos[i]);
					
					if (!pseudoRules[pseudo.name](elem, pseudo))
					{
						return false;
					}
				}			
				return true;
			};
		}(),
	
		' ': function(elems, ss)
		{
			var matched = [];
			var i = elems.length;
			while (i--)
			{
				var descendants = simpleFetch(ss, elems[i]);
				matched = matched.concat(descendants);
				if (ss.id && descendants.length === 1) break;
			}

			return unique(matched);
		},
		
		'>': function(elems, ss)
		{
			var matched = [];
			var i = elems.length;
			while (i--)
			{
				matched = matched.concat(filter(elems[i].childNodes, ss));
			}
			
			return matched;
		},
		
		'+': function(elems, ss)
		{
			var matched = [];
			var i = elems.length;
			while (i--)
			{
				var elem = elems[i];
				var sib = elem.nextSibling;
				while (sib && sib.nodeType !== 1)
				{
					sib = sib.nextSibling;
				}
				if (processSelector(ss, sib))
				{
					matched.push(sib);
				}
			}
			
			return matched;
		},
		
		'~': function(elems, ss)
		{
			genSibBit.inc();
			var bitName = genSibBit.name;
			
			var matched = [];
			var i = elems.length;
			while (i--)
			{
				var elem = elems[i];
				var sib = elem.nextSibling;
				while (sib && sib[bitName] !== genSibBit.t)
				{
					if (sib.nodeType === 1 && processSelector(ss, sib))
					{
						matched.push(sib);						
					}
					sib[bitName] = genSibBit.t;
					sib = sib.nextSibling;
				}
			}
			
			return matched;
		},
		
		',': function(elems, ss)
		{
			return unique(elems.concat(simpleFetch(ss)));
		}
	};

	function makeArray(collection)
	{
		var arr = [];
		var i = collection.length;
		while (i--)
		{
			arr[i] = collection[i];
			callback && callback(arr[i]);
		}
		return arr;
	}
	
	function filter(collection, ss)
	{
		var matched = [];
		var i = collection.length;
		while (i--)
		{
			collection[i] && collection[i].nodeType === 1 && processSelector(ss, collection[i]) && matched.push(collection[i]);
		}
		return matched;
	}
	
	function contains(collection, elem)
	{
		var i = collection.length;
		while (i--)
		{
			if (collection[i] === elem)
			{
				return true;
			}
		}
		return false;
	}

	function unique(collection)
	{
		uniqueBit.inc();
		var name = uniqueBit.name;
		
		var uniqueCollection = [];
		var i = collection.length;
		while (i--)
		{
			var elem = collection[i];
			if (elem[name] !== uniqueBit.t)
			{
				elem[name] = uniqueBit.t;
				uniqueCollection.push(elem);
			}
		}

		return uniqueCollection;
	}
	
	function children(parent)
	{
		return parent.children ||
			Array.prototype.filter && Array.prototype.filter.call(parent.childNodes, function(child) { return child.nodeType === 1; }) ||
			parent.childNodes;
	}
	
	var readAttribute = function()
	{
		// Crappy test until I add back in Gimme's real browser detection!
		return document.all ? ie_readAttribute : w3c_readAttribute;

		function w3c_readAttribute(_elem, _attr)
		{
			return _elem.getAttribute(_attr);
		}

		function ie_readAttribute(_elem, _attr)
		{
			switch (_attr.toLowerCase())
			{
				case 'class':
					return _elem.className || null;

				case 'id':
					return _elem.id || null;

				case 'href':
				case 'src':
					// IE Bug: when a '#default#userData' behavior is bound to an element, getAttribute fails to return anything useful.
					// Worse, it throws a run-time error if you try to invoke it with an iFlag parameter, as in: .getAttribute('href', 2);
					// Even more odd, typeof .getAttribute in this case returns "unknown" ( ?? what the heck! )
					// We can leverage this oddity, to avoid invoking .getAttribute(..) with an iFlag parameter.  In that case, we fall back
					// to trying to retrieve the attribute value through the element's .attributes collection.
					if (typeof _elem.getAttribute !== 'undefined')
					{
						// If typeof .getAttribute is not "unknown" we can safely use iFlag parameters when invoking .getAttribute(..).
						// It still might return null though, (in IE) so a series of logical ORs will help increase our chances of
						// retrieving the desired attribute value.
						return _elem.getAttribute(_attr, 2);
					}
					break;

				default:
					break;
			}

			return _elem.attributes && _elem.attributes[_attr] ? _elem.attributes[_attr].nodeValue : _elem.getAttribute(_attr);
		}
	}();
	
	function CSSParser(rules)
	{
		this.parse = function(input)
		{
			var ppi = rules['preprocessInput'];
			input = ppi && ppi(input) || input;
			
			var part = rules.getNewPart();
			var parts = [];
			
			var c, o, rule;
			
			var i = 0, len = input.length;
			while (i < len)
			{
				c = input.charAt(i);
				rule = rule || rules[c] || rules['default'];
				if (rule)
				{
					o = rule.parse(input.substring(i, len));
					if (!o)
					{
						throw 'No valid match found for "' + rule.name + '" rule';
					}
					i += o.advance;
				}
				else
				{
					throw 'No suitable rule found to handle character "' + c + '"';
				}
				
				if (rule.store(rule.name, o.result, part, parts))
				{
					part = rules.getNewPart();
				}
				rule = null;				
			}
			
			parts.push(part);
			return parts;			
		}
	}
	
	function storeProperty(name, val, part, parts)
	{
		part[name] = val;
		part.propNames.push(name);
		return false;							// means do not reset the parser's current part
	}
	function storeArrayValue(name, val, part, parts)
	{
		if (!part[name]) part[name] = [];
		part[name].push(val);
		part.propNames.push(name);
		return false;							// means do not reset the parser's current part
	}
	function storeCombinator(name, val, part, parts)
	{
		parts.push(part);
		parts.push(val);
		return true;							// means do not reset the parser's current part
	}
	
	var parseRules =
	{
		'preprocessInput': function(input)
		{
			return input.replace(/\s*([ >+~,])\s*/g, '$1');
		},
		
		'getNewPart': function()
		{
			return { propNames: [] };
			//return {};
		},
		
		'default':
		{
			name: 'tag',
			store: storeProperty,
			parse: function(str)
			{
				var m = str.match(/(\w+|\*)/);
				return m && { advance: m[0].length, result: m[0].toUpperCase() } || null;
			}			
		},
		
		'#':
		{
			name: 'id',
			store: storeProperty,
			parse: function(str)
			{
				var m = str.match(/#([_\-\w]+)/);
				return m && { advance: m[0].length, result: m[1] } || null;
			}
		},
		
		'.':
		{
			name: 'classes',
			store: storeArrayValue,
			parse: function(str)
			{
				var m = str.match(/\.([_\-\w\d]+)/);
				return m && { advance: m[0].length, result: m[1] } || null;
			}
		},
		
		'[':
		{
			name: 'attributes',
			store: storeArrayValue,
			parse: function(str)
			{
				var m = str.match(/(\[.*"\]|.*?\])/);
				return m && { advance: m[0].length, result: m[0] } || null;
				
				/*
				var c, m = '', i = 0, len = str.length;
				do
				{
					m += c = str.charAt(i++);
					if (c === '"') do { m += c = str.charAt(i++) } while (c && c !== '"')						
				}
				while (c && c !== ']')
				
				return c && { advance: i, result: m } || null;
				*/
			}
		},
		
		':':
		{
			name: 'pseudos',
			store: storeArrayValue,
			parse: function (str)
			{
				var m = str.match(/:[\w-_]+\b(\(.*\))?/);
				return m && { advance: m[0].length, result: m[0] } || null;
				
				/*
				var c, i;
				var m = str.match(/:([\w-_]+)\b/);
				if (m)
				{
					m = m[1];
					c = str.charAt(i = m.length + 1);
					if (c === '(') do { m += c = str.charAt(i++) } while (c && c !== ')')
				}
				
				return m && { advance: i, result: m } || null;
				*/
			}
		},
		
		' ':
		{
			name: 'desc-combinator',
			store: storeCombinator,
			parse: function(str)			
			{
				return { advance: 1, result: ' ' };				
			}			
		},
		
		'>':
		{
			name: 'child-combinator',
			store: storeCombinator,
			parse: function(str)
			{
				return { advance: 1, result: '>' };
			}
		},
		
		'~':
		{
			name: 'gensib-combinator',
			store: storeCombinator,
			parse: function(str)
			{
				return { advance: 1, result: '~' };
			}
		},
		
		'+':
		{
			name: 'adjsib-combinator',
			store: storeCombinator,
			parse: function(str)
			{
				return { advance: 1, result: '+' };
			}			
		},
		
		',':
		{
			name: 'also-combinator',
			store: storeCombinator,
			parse: function(str)
			{
				return { advance: 1, result: ',' };
			}
		}
	};
	
	var parseRules = parseRules;
	var p = new CSSParser(parseRules);
	
	
	return function(selector, cb)
	{
		callback = cb;
		var args = p.parse(selector);
		nthBit.inc();
		
		var i = 0, len = args.length;
		var elems = simpleFetch(args[i]);
		while (args[++i])
		{
			var combinator = args[i++];
			var rhss = args[i];
			
			elems = search(elems, combinator, rhss);
		}
		
		return elems;
	}

}();